<template>
  <div class="two-side-chart">
    <main></main>
    <section ref="tooltip" v-show="visible">
      <div class="topic" v-text="tooltip.topic"></div>
      <div class="label1" v-text="tooltip.xlabel"></div>
      <div class="label1" v-text="tooltip.ylabel"></div>
    </section>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Ref } from "vue-property-decorator";
import * as d3 from "d3";
import Icon from "../TriangleChart/Icon";

type D3Selection = d3.Selection<any, any, any, any>;
type D3LinearScale = d3.ScaleLinear<any, any, any>;

@Component({
  name: "TwoSideChart",
  components: {}
})
export default class TwoSideChart extends Vue {
  private config!: TwoSideChartConfig;
  private w!: number;
  private h!: number;
  private startX!: number;
  private endX!: number;
  private startY!: number;
  private endY!: number;
  private mdX!: number;
  private xscalelt!: D3LinearScale;
  private xscalert!: D3LinearScale;
  private yscale!: D3LinearScale;
  private svg!: D3Selection;
  private zoom!: D3Selection;
  private defs!: D3Selection;
  private lend!: D3Selection;
  private lt!: D3Selection;
  private rt!: D3Selection;
  private lines!: D3Selection;
  private yaxis!: D3Selection;
  private xaxis!: D3Selection;

  @Ref("tooltip")
  private $tooltip!: HTMLDivElement;

  private visible: boolean = false;

  private tooltip = {
    topic: "",
    xlabel: "",
    ylabel: ""
  };

  public setOptions(config: TwoSideChartConfig) {
    this.config = config;
    this.initParams();
    this.initSvg();
    this.initStyles();
    this.addIconDefs();
    this.addYaxis();
    this.addXaxis();
    this.addGuideLines();
    this.addGuideRects();
    this.addLegends();
    this.addPoints();
    this.bindEvent();
  }

  private initParams() {
    const rect = this.$el.getBoundingClientRect();
    this.w = rect.width;
    this.h = rect.height;

    this.startX = this.config.padding?.left || 0;
    this.endX = this.w - (this.config.padding?.right || 0);
    this.startY = this.config.padding?.top || 0;
    this.endY = this.h - 60 - (this.config.padding?.bottom || 0);
    this.mdX = (this.startX + this.endX) / 2;

    const max = { x: 0, y: 0 };
    this.config.series.forEach((d, i) => {
      if (!d.name) d.name = "分组" + (i + 1);
      d.points.forEach((p) => {
        const x = Math.abs(p.x || 0),
          y = Math.abs(p.y);

        max.x = x > max.x ? x : max.x;
        max.y = y > max.y ? y : max.y;

        p.size = d.icon?.size || Icon.defaultIconSize;
        p.seryName = d.name;
      });
    });

    this.xscalert = d3
      .scaleLinear()
      .range([this.mdX, this.endX])
      .domain([this.config.axies?.x?.origin || 0, 0 - max.x])
      .nice();

    this.yscale = d3
      .scaleLinear()
      .range([this.endY, this.startY])
      .domain([0, 0 - max.y])
      .nice();
  }

  private initSvg() {
    this.svg = d3
      .select(this.$el.querySelector("main") as HTMLDivElement)
      .append("svg")
      .attr("width", "100%")
      .attr("height", "100%");

    this.defs = this.svg.append("defs");
    this.zoom = this.svg.append("g").classed("zoom-ctrl", true);
    this.lend = this.svg.append("g").classed("lend-ctrl", true);

    this.yaxis = this.zoom.append("g").classed("yaxis", true);
    this.xaxis = this.zoom.append("g").classed("xaxis", true);
    this.lt = this.zoom.append("g").classed("lt", true);
    this.rt = this.zoom.append("g").classed("rt", true);
    this.lines = this.zoom.append("g").classed("lines", true);
  }

  private initStyles() {
    this.defs.append("style").text(`
      .yaxis text{fill:${this.config.axies?.y?.color || "#000"}}
      .yaxis line, .yaxis path{stroke:${this.config.axies?.y?.color || "#000"}}
      .ltaxis text{fill:${this.config.axies?.x?.leftAxis?.color || "#000"}}
      .ltaxis line, .ltaxis path{
        stroke:${this.config.axies?.x?.leftAxis?.color || "#000"}
      }
      .rtaxis text{fill:${this.config.axies?.x?.rightAxis?.color || "#000"}}
      .rtaxis line, .rtaxis path{
        stroke:${this.config.axies?.x?.rightAxis?.color || "#000"}
      }
      .data-point{
        cursor:pointer
      }
    `);
  }

  private addYaxis() {
    const axis = d3.axisLeft(this.yscale);
    this.yaxis.call(axis);
    this.yaxis.attr("transform", `translate(${this.mdX}, 0)`);
    if (this.config.axies?.y?.label?.text) {
      this.yaxis
        .append("text")
        .text(this.config.axies.y.label.text)
        .attr("x", -45)
        .attr("y", this.config.padding?.top || 0)
        .style("writing-mode", "tb")
        .style("text-anchor", "start")
        .style("dominant-baseline", "middle")
        .style("font-size", this.config.axies.y.label.fontSize || 14)
        .style("font-weight", this.config.axies.y.label.fontWeight || "bold");
    }
  }

  private addXaxis() {
    const axisrt = d3.axisBottom(this.xscalert);
    const rtg = this.xaxis
      .append("g")
      .classed("rtaxis", true)
      .attr("transform", `translate(0, ${this.endY})`);

    rtg.call(axisrt);

    if (this.config.axies?.x?.rightAxis?.label?.text) {
      rtg
        .append("text")
        .text(this.config.axies.x.rightAxis.label.text + "→")
        .attr("x", this.mdX + 20)
        .attr("y", (this.config.padding?.bottom || 0 + 60) / 2 + 5)
        .style("text-anchor", "start")
        .style("dominant-baseline", "middle")
        .style("font-size", this.config.axies.x.rightAxis.label.fontSize || 14)
        .style(
          "font-weight",
          this.config.axies.x.rightAxis.label.fontWeight || "bold"
        );
    }

    const domain = this.xscalert.domain();
    this.xscalelt = d3
      .scaleLinear()
      .domain([domain[1], domain[0]])
      .range([this.startX, this.mdX]);

    const axislt = d3.axisBottom(this.xscalelt);
    this.xaxis
      .append("g")
      .classed("ltaxis", true)
      .attr("transform", `translate(0, ${this.endY})`)
      .call(axislt);

    if (this.config.axies?.x?.leftAxis?.label?.text) {
      rtg
        .append("text")
        .text("←" + this.config.axies.x.leftAxis.label.text)
        .attr("x", this.mdX - 20)
        .attr("y", (this.config.padding?.bottom || 0 + 60) / 2 + 5)
        .style("text-anchor", "end")
        .style("dominant-baseline", "middle")
        .style("font-size", this.config.axies.x.leftAxis.label.fontSize || 14)
        .style(
          "font-weight",
          this.config.axies.x.leftAxis.label.fontWeight || "bold"
        );
    }
  }

  private addIconDefs() {
    this.config.series.forEach((d) => {
      d.id = Icon.createIconMapping(d.icon || {}, this.defs);
    });
  }

  private addPoints() {
    const ltPoints: TwoSideChartPoint[] = [];
    const rtPoints: TwoSideChartPoint[] = [];
    this.config.series.forEach((d) => {
      d.points.forEach((p) => {
        p.seryId = d.id + "";
        if (p.side === "left") {
          ltPoints.push({ ...p });
        } else {
          rtPoints.push({ ...p });
        }
      });
    });

    this.rt
      .selectAll("use")
      .data(rtPoints)
      .enter()
      .append("use")
      .classed("data-point", true)
      .attr(
        "x",
        (d) => this.xscalert(d.x) - (d.size || Icon.defaultIconSize) / 2
      )
      .attr("y", (d) => this.yscale(d.y) - (d.size || Icon.defaultIconSize) / 2)
      .attr("xlink:href", (d) => "#" + d.seryId)
      .attr("width", (d) => d.size || Icon.defaultIconSize)
      .attr("height", (d) => d.size || Icon.defaultIconSize);

    this.lt
      .selectAll("use")
      .data(ltPoints)
      .enter()
      .append("use")
      .classed("data-point", true)
      .attr(
        "x",
        (d) => this.xscalelt(d.x) - (d.size || Icon.defaultIconSize) / 2
      )
      .attr("y", (d) => this.yscale(d.y) - (d.size || Icon.defaultIconSize) / 2)
      .attr("xlink:href", (d) => "#" + d.seryId)
      .attr("width", (d) => d.size || Icon.defaultIconSize)
      .attr("height", (d) => d.size || Icon.defaultIconSize);
  }

  private addGuideLines() {
    if (!this.config.guideLines?.lines) return;
    const rtlines = this.lines.append("g").classed("rtlines", true);
    const ltlines = this.lines.append("g").classed("ltlines", true);
    const lines = [
      this.config.guideLines.lines.upLine,
      this.config.guideLines.lines.downLine
    ];
    lines.forEach((d) => {
      const k = (d.y2 - d.y1) / (d.x2 - d.x1);

      d.k = k;
      d.b = d.y1 - k * d.x1;

      const px1 = this.config.axies?.x?.origin || 0;

      const py1 = k * (px1 - d.x1) + d.y1;
      const px2 = this.xscalert.domain()[1];
      const py2 = k * (px2 - d.x1) + d.y1;

      rtlines
        .append("line")
        .attr("x1", this.xscalert(px1))
        .attr("y1", this.yscale(py1))
        .attr("x2", this.xscalert(px2))
        .attr("y2", this.yscale(py2))
        .style("stroke", this.config.guideLines?.color || "#000")
        .style("stroke-width", this.config.guideLines?.width || "2");

      ltlines
        .append("line")
        .attr("x1", this.xscalelt(px1))
        .attr("y1", this.yscale(py1))
        .attr("x2", this.xscalelt(px2))
        .attr("y2", this.yscale(py2))
        .style("stroke", this.config.guideLines?.color || "#000")
        .style("stroke-width", this.config.guideLines?.width || "2");

      if (d.path) {
        d.path.forEach((x, i) => {
          const x1 = x.x1;
          const y1 = k * (x1 - d.x1) + d.y1;
          const x2 = x.x2;
          const y2 = k * (x2 - d.x1) + d.y1;

          const xrt1 = this.xscalert(x1),
            yrt1 = this.yscale(y1),
            xrt2 = this.xscalert(x2),
            yrt2 = this.yscale(y2);

          const rtpath = d3.path();
          rtpath.moveTo(xrt1, yrt1);
          rtpath.quadraticCurveTo(
            (xrt1 + xrt2) / 2 + 80,
            (yrt1 + yrt2) / 2 + 90,
            xrt2,
            yrt2
          );
          rtpath.closePath();

          rtlines
            .append("path")
            .attr("d", rtpath.toString())
            .style("stroke", this.config.guideLines?.color || "#000")
            .style("stroke-width", this.config.guideLines?.width || "2")
            .style("fill", "none")
            .attr("id", "rtcircle" + i);

          const $path = document.querySelector(
            "#rtcircle" + i
          ) as SVGPathElement;
          const len = $path.getTotalLength();

          const p = d3.path();
          const data: Array<{ x: number; y: number }> = [];
          for (let i = 0; i < 200; i++) {
            const point = $path.getPointAtLength((i / 200) * len);
            const x = this.xscalert.invert(point.x);
            data.push({ x, y: this.yscale.invert(point.y) });
            if (i === 0) {
              p.moveTo(this.xscalelt(x), point.y);
            } else {
              p.lineTo(this.xscalelt(x), point.y);
            }
          }
          ltlines
            .append("path")
            .attr("d", p.toString())
            .style("stroke", this.config.guideLines?.color || "#000")
            .style("stroke-width", this.config.guideLines?.width || "2")
            .style("fill", "none")
            .attr("id", "rtcircle" + i);

          const maxx = d3.max(data, (d) => d.x) as number,
            minx = d3.min(data, (d) => d.x) as number,
            maxy = d3.max(data, (d) => d.y) as number,
            miny = d3.min(data, (d) => d.y) as number;

          rtlines
            .append("text")
            .text("Ⅲ")
            .attr("x", this.xscalert((maxx + minx) / 2))
            .attr("y", this.yscale((maxy + miny) / 2))
            .style("text-anchor", "start")
            .style("dominant-baseline", "middle")
            .style("font-size", 14)
            .style("fill", this.config.guideLines?.color || "#000");

          ltlines
            .append("text")
            .text("Ⅲ")
            .attr("x", this.xscalelt((maxx + minx) / 2))
            .attr("y", this.yscale((maxy + miny) / 2))
            .style("text-anchor", "end")
            .style("dominant-baseline", "middle")
            .style("font-size", 14)
            .style("fill", this.config.guideLines?.color || "#000");
        });
      }
    });
  }

  private addRect(
    dxmax: number,
    dxmin: number,
    dymax: number,
    dymin: number,
    box: D3Selection,
    xscale: D3LinearScale,
    yscale: D3LinearScale,
    line: TwoSideChartGuideLine
  ) {
    const xmax = xscale(dxmax),
      xmin = xscale(dxmin),
      ymax = yscale(dymax),
      ymin = yscale(dymin);

    const path = d3.path();
    path.moveTo(xmax, ymax);
    path.lineTo(xmin, ymax);
    path.lineTo(xmin, ymin);
    path.lineTo(xmax, ymin);
    path.closePath();

    box
      .append("path")
      .attr("d", path.toString())
      .style("fill", "none")
      .style("stroke", this.config.guideLines?.color || "#000")
      .style("stroke-width", this.config.guideLines?.width || "2");

    box
      .append("text")
      .text("Ⅳ")
      .attr("x", d3.mean([xmax, xmin]) as number)
      .attr("y", d3.mean([ymax, ymin]) as number)
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .style("font-size", 14)
      .style("fill", this.config.guideLines?.color || "#000");

    const dx0 = (dymin - (line.b || 0)) / (line.k || 1);
    const x0 = xscale(dx0);

    path.moveTo(xmax, ymin);
    path.lineTo(x0, ymin);
    box
      .append("path")
      .attr("d", path.toString())
      .style("fill", "none")
      .style("stroke", this.config.guideLines?.color || "#000")
      .style("stroke-width", this.config.guideLines?.width || "2");

    box
      .append("text")
      .text("Ⅱ")
      .attr("x", () => {
        if (box.classed("lt-rect")) {
          return xmin - 30;
        } else {
          return xmin + 30;
        }
      })
      .attr("y", (ymax + ymin) / 2 - 50)
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .style("font-size", 14)
      .style("fill", this.config.guideLines?.color || "#000");

    box
      .append("text")
      .text("Ⅰ")
      .attr("x", () => {
        if (box.classed("lt-rect")) {
          return xmax + 30;
        } else {
          return xmax - 30;
        }
      })
      .attr("y", (ymax + ymin) / 2 + 50)
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .style("font-size", 14)
      .style("fill", this.config.guideLines?.color || "#000");
  }

  private addGuideRects() {
    if (!this.config.guideLines) return;

    const config = this.config.guideLines.rect;

    const upLine = this.config.guideLines.lines.upLine;
    const downLine = this.config.guideLines.lines.downLine;
    const rect = this.config.guideLines.rect;

    const xmax = rect.xmax;
    const xmin = rect.xmin;
    const ymax = (downLine.k || 1) * rect.xmin + (downLine.b || 0);
    const ymin = config.y;

    const box = this.zoom.append("g").classed("rects", true);
    const ltbox = box.append("g").classed("lt-rect", true);
    const rtbox = box.append("g").classed("rt-rect", true);

    this.addRect(
      xmax,
      xmin,
      ymax,
      ymin,
      rtbox,
      this.xscalert,
      this.yscale,
      upLine
    );
    this.addRect(
      xmax,
      xmin,
      ymax,
      ymin,
      ltbox,
      this.xscalelt,
      this.yscale,
      upLine
    );
  }

  private addLegends() {
    const legends = this.lend
      .selectAll("g")
      .data(this.config.series)
      .enter()
      .append("g")
      .classed("legend-item", true)
      .attr("transform", (d, i) => `translate(${this.w - 30}, ${i * 25 + 20})`);

    legends
      .append("text")
      .text((d, i) => d.name || "分组" + i)
      .attr("x", -10)
      .style("text-anchor", "end")
      .style("dominant-baseline", "middle");

    legends
      .append("use")
      .attr("x", 0)
      .attr("y", -10)
      .attr("width", 20)
      .attr("height", 20)
      .attr("xlink:href", (d) => "#" + d.id);
  }

  private bindEvent() {
    const _this = this;
    this.zoom
      .selectAll(".data-point")
      .on("mouseover", function (this, e: MouseEvent, d: any) {
        const data = d as TwoSideChartPoint;
        _this.tooltip.topic = data.seryName + "";
        if (data.name) {
          _this.tooltip.topic = _this.tooltip.topic + " -- " + data.name;
        }

        if (data.side === "left") {
          _this.tooltip.xlabel =
            _this.config.axies?.x?.leftAxis?.label?.text + " : " + data.x;
        } else {
          _this.tooltip.xlabel =
            _this.config.axies?.x?.rightAxis?.label?.text + " : " + data.x;
        }
        _this.tooltip.ylabel =
          _this.config.axies?.y?.label?.text + " : " + data.y;

        _this.$tooltip.style.left = e.clientX + 15 + "px";
        _this.$tooltip.style.top = e.clientY + 15 + "px";

        _this.visible = true;
      })
      .on("mouseout", () => {
        this.visible = false;
      });
  }
}
</script>
<style lang="scss" scoped>
.two-side-chart {
  width: 100%;
  height: 100%;
  main {
    width: 100%;
    height: 100%;
  }
  section {
    position: fixed;
    z-index: 9999;
    left: 0;
    min-width: 150px;
    top: 0;
    background-color: rgba(0, 0, 0, 0.8);
    color: #fff;
    text-align: left;
    padding: 10px;
    border-radius: 6px;
    transition: all ease 0.3s;
    .topic {
      font-size: 14px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .label1 {
      font-size: 12px;
      & + .label1 {
        margin-top: 7px;
      }
    }
  }
}
</style>
